Skip to main content
Version: Mosquitto 2.9

JWT Authentication

Premium

Introduction

The JWT Authentication plugin can be used to authenticate MQTT clients using JSON web tokens instead of a password.

To use the plugin, a configuration file must be specified (see example configuration for an example of such file and config file format section for the overview of all possible configuration parameters)

The plugin uses a common secret or a public key to verify the signature of the provided token. If the signature is valid, the client gets authenticated.

Plugin Activation

To enable the JWT Auth plugin on the broker, add the following to the mosquitto.conf file:

plugin /usr/lib/cedalo_jwt_auth.so

persistence_location /mosquitto/data

This is an example configuration snippet applicable to the docker container setup. For installations not running in a container the above configuration needs to be adjusted accordingly (namely the location of cedalo_jwt_auth.so dynamic library and persistence_location may differ).

persistence_location is used as a search base path for the plugin's config file.

In addition to modifying mosquitto.conf, ensure that you have the appropriate license to use the plugin.

Config File Format

The configuration is stored in a single JSON file (called jwt.json by default) located inside the persistence_location, which is defined in mosquitto.conf. To use a different config file name, specify the plugin_opt_config_file option with the custom file name under the JWT plugin section in mosquitto.conf.

The file represents a configuration object consisting of a selected authentication mode for the plugin, a signature algorithm, and an object with corresponding options.

The fields of the configuration object are described below. To see the entire structure of the configuration file, take a look at the JSON schema at the bottom of this page. To see an example of the config file refer to the configuration example section.

The following fields of the config are mandatory:

  • algorithm (path: $.algorithm):
    Algorithm that was used to sign the token. Either RS256 in case a public key was used or HS256 in case of a shared secret (type: string, one of: RS256, HS256).
  • mode (path: $.mode):
    Mode of the authentication. Either jwks or local. jwks mode calls a special JSON Web Key Set (JWKS) endpoint provided by your authentication/OAuth provider to get public keys for token validation. Alternatively, JWKS can also be fetched through a file on the filesystem. local mode is used to specify a shared secret or a public key file in PEM format (type: string, one of: jwks, local).
  • options (path: $.options):
    Further settings of the plugin (type: object).
    • modes (path: $.options.modes):
      Section that details settings of one of the chosen auth modes (jwks or local) (type: object).
      • jwks (path: $.options.modes.jwks):
        Settings for the jwks mode. This option is set in case jwks mode is chosen in $.mode. It accepts one of the following mandatory parameters. (type: object).
        • uri (path: $.options.modes.jwks.uri):
          URI of the JWKS endpoint including protocol, port, path, and query parameters (if any) (type: string).
        • filePath (path: $.options.modes.jwks.filePath):
          This option provides an alternative to $.options.modes.jwks.uri. It specifieds a full path to a file where JWKS is being stored (type: string).
      • local (path: $.options.modes.local):
        Settings for the local mode. This option is set in case local mode is chosen in $.mode. It can be either a plain text string in case it is acceptable to specify a shared secret or a public key in plain text or it can also be an object with one of the properties below. (type: object or string)
        • plain (path: $.options.modes.local.palin):
          A plaintext shared secret or public key in PEM format with PEM headers. \n can be used to separate PEM headers from the key string. (type: string).
        • env (path: $.options.modes.local.env):
          A name of the environment variable where a shared secret or a public key is stored (type: string).
        • filePath (path: $.options.modes.local.filePath):
          A full path to a file where a shared secret or a public key is being stored (type: string).

Optional fields:

  • version (path: $.version):
    Version of the configuration file. Reserved for future use (type: string, defaults to: 1).
  • options (path: $.options):
    (type: object).
    • modes (path: $.options.modes):
      (type: object).
      • jwks (path: $.options.modes.jwks):
        (type: object).
        • timeoutMs (path: $.options.modes.jwks.timeoutMs):
          Timeout in milliseconds for fetching JSON web key set via a network request (type: number, defaults to: 10000).
        • cache (path: $.options.modes.jwks.cache):
          Cache options when fetching JWKS via a network request (type: object).
          • enabled (path: $.options.modes.jwks.cache.enabled):
            Whether to enable cache for JWKS requests (type: bool, defaults to: false).
          • ttlMs (path: $.options.modes.jwks.cache.ttlMs):
            Time to live in milliseconds for cache entries. This means that the result of the JWKS request will be cached for the specified time (type: number, defaults to: 60000).
          • respectCacheHeaders (path: $.options.modes.jwks.cache.respectCacheHeaders):
            Whether to use cache header provided in response to the JWKS request. The cache headers specify the time for which the response should be cached and will take precedence over ttlMs setting (type: bool, defaults to: false).
    • common (path: $.options.common):
      Common options used for both jwks and local auth modes (type: object).
      • issuer (path: $.options.common.issuer):
        A specific issuer that iss property of the token will be matched against. The authentication will fail if the values don't match (type: string).
      • validateExpiration (path: $.options.common.validateExpiration):
        Whether to reject expired tokens (type: bool, defaults to: true).
      • matchUsernameToSubject (path: $.options.common.matchUsernameToSubject):
        Whether to match username provided by the MQTT client against the sub field of the token. In case these fields do not match, the authentication is denied (type: bool, defaults to: false).
      • matchClientIdToSubject (path: $.options.common.matchClientIdToSubject):
        Whether to match client id of the MQTT client against the sub field of the token. In case these fields do not match, the authentication is denied (type: bool, defaults to: false).
      • kickExpiredClients (path: $.options.common.kickExpiredClients):
        Whether to kick MQTT clients whose token expires while they are still connected to the broker (type: bool, defaults to: true).
      • kickExpiredClientsWithWill (path: $.options.common.kickExpiredClientsWithWill):
        Whether to send out last will messages for the clients that are kicked due to kickExpiredClients option being true (type: bool, defaults to: true).
    • lazyConnect (path: $.options.lazyConnect):
      Whether to ignore checking JWKS and revocation endpoint reachability during the broker startup. If this option is set to false and one of these endpoints is defined in the configuration but is unreachable, then the plugin will fail and become unoperational. This check only happens once during startup and is made to spot connectivity issues and malformed URIs early on. The reachability check is performed by sending a HEAD HTTP request to the endpoints of interest. (type: bool, defaults to: false).
    • revocation (path: $.options.revocation):
      Options related to the revocation check endpoint, which checks whether the token was revoked (for instance, due to being compromised) before verifying it on the broker. The revocation object must contain either the introspection or plain option (type: object).
      • introspection (path: $.options.revocation.introspection):
        Options for configuring request to the introspection endpoint. Introspection endpoint is a standardized way of fetching more information about the token (including its revocation status) and is typically provided by authentication/OAuth providers (type: object).
        • uri (path: $.options.revocation.introspection.uri):
          URI of the introspection endpoint, including protocol, port, path, and query parameters (if any). For instance, http://keycloak:8080/realms/test/protocol/openid-connect/token/introspect (type: string).
        • clientId (path: $.options.revocation.introspection.clientId):
          Client ID to be used to access the introspection endpoint. Client ID as well as the client secret are base64 encoded into the basic authorization header of the POST request. The token, on the other hand, is url encoded into the request body as an object with a single property called token (type: string).
        • clientSecret (path: $.options.revocation.introspection.clientSecret):
          Client secret to be used to access the introspection endpoint. Client secret as well as client ID are base64 encoded into the basic authorization header of the POST request. (type: string).
        • requestParameters (path: $.options.revocation.introspection.requestParameters):
          Additional parameters to be sent with the introspection request. This may include additional headers or define a different request payload (body) structure. More information on requestParameters can be found below at the bottom of this section (type: object).
          • plain (path: $.options.revocation.introspection.requestParameters.plain):
            Request parameters as an object or a string representation of a JSON object (type: object or string).
          • env (path: $.options.revocation.introspection.requestParameters.env):
            Name of the environment variable from which request parameters are to be fetched. This can be useful if request parameters include sensitive information. Request parameters can be stored in the environment variable as a string representation of a JSON object (type: string).
          • filePath (path: $.options.revocation.introspection.requestParameters.filePath):
            Full path to the file on the file system from which reqeuest parameters are to be fetched. The parameters in the file must be in JSON fomat (type: string).
        • includeTokenTypeHint (path: $.options.revocation.introspection.includeTokenTypeHint):
          Whether to include token_type_hint property with a value of access_token into the body of the introspection request (type: bool, defaults to: false).
      • plain (path: $.options.revocation.plain):
        Options for configuring requests to the "plain" revocation check endpoint. This options can be used in case a custom revocation enpoint is needed instead of a standardized introspection endpoint (type: object).
        • uri (path: $.options.revocation.plain.uri):
          URI of the custom revocation endpoint. Including protocol, port, path and query params (if any) (type: string).
        • requestParameters (path: $.options.revocation.plain.requestParameters): Custom request parameters (i.e. headers, payload, path, or method) for the revocation endpoint. See the bottom of this section for more information.
          (type: object).
          • plain (path: $.options.revocation.plain.requestParameters.plain):
            Request parameters as an object or a string representation of a JSON object (type: object or string).
          • env (path: $.options.revocation.plain.requestParameters.env):
            Name of the environment variable from which request parameters are to be fetched. This can be useful if request parameters include sensitive information. Request parameters can be stored in the environment variable as a string representation of a JSON object (type: string).
          • filePath (path: $.options.revocation.plain.requestParameters.filePath):
            Full path to the file on the file system from which reqeuest parameters are to be read. The parameters in the files must be stored in JSON format (type: string).
        • bodyEncoding (path: $.options.revocation.plain.bodyEncoding):
          Type of body endoding of the request to the revocation check endpoint (type: string, one of: application/x-www-form-urlencoded, application/json, default to: application/x-www-form-urlencoded).
        • response (path: $.options.revocation.plain.response):
          Object containing options for validating response from the custom revocation check endpoint. These options specify a way of determining whether revocation check succeeded of failed (type: object).
          • jsonPath (path: $.options.revocation.plain.response.jsonPath):
            JSON path to the property in the response body that determines the success of the revocation check. For example, the value of this option can be validation.isSuccess which will retrieve isSuccess property of the validation object from the body of the respose for further inspection (type: string).
          • successValue (path: $.options.revocation.plain.response.successValue):
            Value that the property at jsonPath will be matched against to determine the success of the revocation check. successValue should be defined together with jsonPath to avoid undefined behavior. In case both jsonPath and successValue are not defined, the success of the revocation check is determined by the HTTP response status codes from successHttpCodes (type: string or bool or number).
          • successHttpCodes (path: $.options.revocation.plain.response.successHttpCodes):
            An array of HTTP status codes that are treated as sucessful responses. In case an HTTP status which is not present in this list is encountered, the revocation check fails regardless of jsonPath and successValue properties. (type: array of type: number, defaults to: [200, 201, 202, 203, 204, 205, 206, 207, 208, 226]).
      • timeoutMs (path: $.options.revocation.timeoutMs):
        Timeout of the request to the custom revocation check endpoint in milliseconds (type: number, defaults to: 10000).

You can also find all the available configuration options as a schema here

Request parameters value in $.options.revocation.plain.requestParameters and $.options.revocation.introspection.requestParameters can be represented as a valid JSON string or an actual JSON object. To represent it as a valid JSON object it can be put directly into the configuration file under requestParameters.plain. In case it's represented as a string, it can be stored in an environment variable referenced by requestParameters.env or in a file referenced by requestParameters.filePath. However, it is worth noting that a valid JSON string representing request parameters can also be stored directly under requestParameters.plain.

Request parameters object can contain the following properties:

  • payload: Contents of the request body (typically a JSON object, but other types like string or number can also be used).
  • headers: An object of key value pairs representing HTTP headers.
  • method: HTTP verb to use when requesting revocation check endpoint: GET, POST, PUT, PATCH, or DELETE.
  • path: Overrides an HTTP path of the request (including query parameters if any). Should start with /. However, the path can also be entered directly into the uri field.

If value of the token needs to be inserted into any of the headers, path, or payload, a special string #:jwt_value:# can be used.

Example of request parameters:

{
...
"requestParameters": {
"plain": {
"headers": {
"Token": "Access token: #:jwt_value:#",
"HTTPHeader2": "value",
"HTTPHeader3": 1
},
"payload": {
"role": "client",
"token": {
"value": "#:jwt_value:#"
}
},
"path": "/revocation/check/#:jwt_value:#?token_query_param=#:jwt_value:#&param2=some_value",
"method": "POST"
}
}
...
}

In the example above, string #:jwt_value:# will be replaced with the value of the JSON web token provided by a client. The revocation check will be made as a POST request to requestParameters.plain.path using the headers specified in requestParameters.plain.headers and a payload from requestParameters.plain.payload.

Configuration example

An example of the jwt.json config file is shown below:

{
"version": "1",
"algorithm": "RS256",
"mode": "jwks",
"options": {
"lazyConnect": false,
"modes": {
"jwks": {
"uri": "http://keycloak:8080/realms/test/protocol/openid-connect/certs",
"timeoutMs": 5000,
"cache": {
"enabled": true,
"ttlMs": 30000,
"respectCacheHeaders": false
}
}
},
"common": {
"validateExpiration": true,
"matchUsernameToSubject": true,
"matchClientIdToSubject": false,
"kickExpiredClients": true,
"kickExpiredClientsWithWill": true
},
"revocation": {
"introspection": {
"uri": "http://keycloak:8080/realms/test/protocol/openid-connect/token/introspect",
"clientId": "client-1",
"clientSecret": "BADQQjnPfdJrAW9jkIOJzCSidW9me5JH",
"includeTokenTypehint": false
},
"timeoutMs": 5000
}
}
}

Given this example config, whenever the broker starts up, the JWT Authentication plugin will test reachability of the JWKS endpoint at http://keycloak:8080/realms/test/protocol/openid-connect/certs as well as introspection endpoint at http://keycloak:8080/realms/test/protocol/openid-connect/token/introspect by performing HEAD HTTP requests. If at least one of those endpoints is unreachable, the plugin will fail and output an error. Otherwise, it will load normally and wait for authentication request from the MQTT clients. If you don't want the plugin to be able to fail in case one of the endpoints is unreachable on startup, change the lazyConnect setting to true.

After receiving an authentication request from an MQTT client, the broker will retrieve the token from the password field and include it (together with the clientId and clientSecret config fields) into a request to the introspection endpoint at http://keycloak:8080/realms/test/protocol/openid-connect/token/introspect. This request will determine whether the token is revoked or not. In case the token is revoked or request to the introspection endpoint times out after 5 seconds (5000 milliseconds), the authentication request is denied. Otherwise, the broker fetches public keys from the JWKS endpoint at http://keycloak:8080/realms/test/protocol/openid-connect/certs, caching them for 30 seconds ("ttlMs": 30000), and then, using those keys, verifies the signature of the JSON Web Token. If JWKS endpoint doesn't respond for 5 seconds (5000 milliseconds), the authentication is denied. Otherwise and if the signature is valid, the username of the client is also matched against the sub field of the token (as per matchUsernameToSubject setting). If these match, the client is authenticated to the broker. Then the timer is set up to kick the client out after the token expires (as per kickExpiredClients setting). If the client is kicked out, the last will message will be sent out as per kickExpiredClientsWithWill setting.

Below is another very short config example which uses local mode and a plaintext shared secret with a value secret. The fact that shared secret is used is also evident by the use of HS256 algorithm instead of RS256 which is applied for public keys. This config doesn't use any revocation endpoints or specify any common settings, relying on defaults instead:

{
"version": "1",
"algorithm": "HS256",
"mode": "local",
"options": {
"modes": {
"local": "secret"
}
}
}

Error handling

Any configuration or license errors will prevent the plugin from loading, and corresponding error messages will be logged.

If recoverable errors occur during the operation of the plugin, it will generate respective error messages prefixed with ERR: in the logs.

Notable behavior

If the token expires while a client has an active session with a broker, the client will be kicked out by default, or if the kickExpiredClients option is explicitly set to true in the configuration file. To avoid being kicked out, clients must be aware of when their tokens are about to expire, refresh them before expiration, and reauthenticate with the broker. Setting kickExpiredClients to false is another way to prevent this behavior, but it is not generally recommended due to security considerations.

Limitations

JWT Auth plugin currently supports authentication only. ACLs are not supported and should be specified using other means, such as an ACL file.

JSON Schema

Schema for all possible parameters for the jwt.json config file:

{
"title": "JWT Auth Plugin Config",
"type": "object",
"properties": {
"version": {
"type": "string",
"default": "1",
"description": "Config file version. If the field is ommited, version 1 is assumed",
"nullable": true
},
"algorithm": {
"type": "string",
"enum": ["RS256", "HS256"],
"nullable": false,
"description":
"Algorithm used for signing JWT tokens. Either \"HS256\" for shared secrets or \"RS256\" for public keys"
},
"mode": {
"type": "string",
"enum": ["local", "jwks"] ,
"nullable": false,
"description":
"Way in which JWT will obtain secret/key information. Either through the JWKS endpoint/file or thorugh a local shared secret/public key. Values are either \"local\" or \"jwks\""
},
"options": {
"type": "object",
"description": "Settings of the plugin",
"properties": {
"lazyConnect": {
"description": "Whether to ignore checking JWKS and JWT revocation endpoints reachability on plugin startup or not",
"type": "boolean",
"default": false,
"nullable": true
},
"modes": {
"type": "object",
"description": "Settings for the different modes of the plugin (jwks and local)",
"properties": {
"jwks": {
"type": "object",
"description": "Settings for the jwks mode",
"properties": {
"uri": {
"type": "string",
"description": "URI of the JWKS endpoint",
"nullable": true
},
"timeoutMs": {
"type": "number",
"description":
"Timeout in milliseconds for the request to the JWKS endpoint",
"nullable": true,
"default": 10000
},
"cache": {
"type": "object",
"description": "Settings for caching the JWKS keys",
"properties": {
"enabled": {
"type": "boolean",
"description": "Whether to cache the JWKS keys",
"nullable": true,
"default": false
},
"ttlMs": {
"type": "number",
"description":
"Number of milliseconds the JWKS keys are cached for",
"nullable": true,
"default": 60000
},
"respectCacheHeaders": {
"type": "boolean",
"description":
"Whether to respect cache headers when making a request to the JWKS endpoint",
"nullable": true,
"default": false
}
},
"nullable": true
},
"filePath": {
"type": "string",
"description":
"Path to the file containing the JWKS keys as an alternative to specifying a uri and making an external request",
"nullable": true
}
},
"nullable": true
},
"local": {
"oneOf": [
{
"type": "string",
"nullable": true,
"description":
"Signing key in plaintext. Signing key can either be a shared secret or a public key (depending on the algorithm)",
},
{
"type": "object",
"description":
"Settings for signing keys of the JWT when local mode is used (signing keys are stored locally in a plain text format)",
"properties": {
"plain": {
"type": "string",
"description":
"Signing key in plaintext. Signing key can either be a shared secret or a public key (depending on the algorithm)",
"nullable": true
},
"env": {
"type": "string",
"description":
"Name of the environment variable containing the signing key",
"nullable": true
},
"filePath": {
"type": "string",
"description": "Path to the file containing the signing key",
"nullable": true
}
},
"nullable": true
}
]
}
}
},
"common": {
"type": "object",
"description": "Common settings for both modes",
"properties": {
"issuer": {
"type": "string",
"description": "A particular issuer of the JWT that the broker will check for",
"nullable": true
},
"validateExpiration": {
"type": "boolean",
"description": "Whether to validate the expiration time of the JWT",
"nullable": true,
"default": true
},
"matchUsernameToSubject": {
"type": "boolean",
"description":
"Whether to match username of the MQTT client to the sub field of the JWT",
"nullable": true,
"default": false
},
"matchClientIdToSubject": {
"type": "boolean",
"description":
"Whether to match client id of the MQTT client to the sub field of the JWT",
"nullable": true,
"default": false
},
"kickExpiredClients": {
"type": "boolean",
"description": "Whether to kick out clients whose JWT has expired",
"nullable": true,
"default": true
},
"kickExpiredClientsWithWill": {
"type": "boolean",
"description":
"Whether to send out a last will testament when the client is kicked out due to JWT expiration",
"nullable": true,
"default": true
}
},
"nullable": true
},
"revocation": {
"type": "object",
"nullable": true,
"description": "Settings for checking for JWT revocation",
"properties": {
"introspection": {
"type": "object",
"description": "OAuth introspection endpoint settings",
"properties": {
"uri": {
"type": "string",
"description": "URI of the introspection endpoint. including protocol, port, path, and query params (if any)",
"nullable": false
},
"clientId": {
"oneOf": [
{
"type": "string",
"nullable": false,
"description":
"Client id in plaintext",
},
{
"type": "object",
"description":
"Client id to be included into the request to the introspection endpoint",
"properties": {
"plain": {
"type": "string",
"description":
"Client id in plaintext",
"nullable": false
},
"env": {
"type": "string",
"description":
"Name of the environment variable containing the client id",
"nullable": false
},
"filePath": {
"type": "string",
"description":
"Path to the file containing the client id",
"nullable": false
}
},
"nullable": false
}
]
},
"clientSecret": {
"oneOf": [
{
"type": "string",
"nullable": false
},
{
"type": "object",
"description":
"Client secret to be included into the request to the introspection endpoint",
"properties": {
"plain": {
"type": "string",
"description":
"Client secret in plaintext",
"nullable": true
},
"env": {
"type": "string",
"description":
"Name of the environment variable containing the client secret",
"nullable": true
},
"filePath": {
"type": "string",
"description":
"Path to the file containing the client secret",
"nullable": true
}
},
"nullable": false
}
]
},
"requestParameters": {
"type": "object",
"description":
"Additional parameters to be included into the request to the introspection endpoint",
"properties": {
"plain": {
"oneOf": [
{ "type": "string" },
{
"type": "object",
"additionalProperties": true
}
],
"description":
"Plain value of the request parameters. Can be an object or a stringified json"
},
"env": {
"type": "string",
"description":
"Name of the environment variable containing the request parameters",
"nullable": true
},
"filePath": {
"type": "string",
"description":
"Path to the file containing the request parameters",
"nullable": true
}
},
"nullable": true
},
"includeTokenTypeHint": {
"type": "boolean",
"description":
"Whether to include token_type_hint=\"access_token\" property when making request to the introspection endpoint",
"default": false,
"nullable": true
}
},
"nullable": true,
"required": ["uri", "clientId", "clientSecret"]
},
"plain": {
"type": "object",
"description": "Plain endpoint settings used to check for JWT revocation. These are essentially settings for a custom revocation check endpoint",
"properties": {
"uri": {
"type": "string",
"description": "URI of the endpoint for the revocation check",
"nullable": false
},
"requestParameters": {
"type": "object",
"description":
"Additional parameters to be included into the JWT revocation check request",
"properties": {
"plain": {
"type": "string",
"description":
"Plain value of the request parameters. Can be an object or a stringified json",
"nullable": true
},
"env": {
"type": "string",
"description":
"Name of the environment variable containing request parameters as a stringified JSON",
"nullable": true
},
"filePath": {
"type": "string",
"description":
"Path to the file containing the request parameters",
"nullable": true
}
},
"nullable": true
},
"bodyEncoding": {
"type": "string",
"nullable": true,
"enum": ["application/x-www-form-urlencoded", "application/json"],
"default": "application/x-www-form-urlencoded",
"description":
"Whether to include and encode body of the revocation check request"
},
"response": {
"type": "object",
"description":
"Settings for the response of the JWT revocation check",
"properties": {
"checkHttpStatus": {
"type": "boolean",
"description":
"Whether to validate JWT revocation status based on the HTTP status. 2XX HTTP codes mean that the JWT was not revoked. If \"response\" section is field blank or ommited, then successfulness of the request will depend solely on the response status code",
"nullable": true
},
"jsonPath": {
"type": "string",
"description":
"JSON path to the value in the response that will be checked for success. If no value is provided then the entire payload will be matched against the successValue (if successValue is provided)",
"nullable": true
},
"successValue": {
"type": ["string", "number", "boolean"],
"description":
"Value returned from the JWK revocation check endpoint that indicates that the JWT is valid and not revoked. If no value is provided but jsonPath is specified then the broker will check for the truthy value",
"nullable": true
},
"successHttpCodes": {
"type": "array",
"items": {
"type": "number"
},
"description":
"HTTP status codes which are counted as successful when making request to the plain endpoint. By default these are all the 2XX status codes",
"nullable": true
}
},
"nullable": true
}
},
"nullable": true,
"required": ["uri"]
},
"timeoutMs": {
"type": "number",
"description":
"Timeout in milliseconds for the request to the JWT revocation check endpoint",
"nullable": true,
"default": 10000
}
}
}
},
"required": ["modes"]
}
},
"required": ["algorithm", "mode", "options"]
}